﻿using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Metadata;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Messages;
using DLaB.Xrm;
using Microsoft.Xrm.Sdk.Query;
using DLaB.Xrm.Entities;
using DLaB.Xrm.Common.Exceptions;
using DLaB.Xrm.Core.Entities;


namespace DLaB.LocalToGlobalOptionSet
{
    public class Logic
    {
        public delegate void LogHandler(string text);
        public event LogHandler OnLog;

        public bool MigrateData { get; set; }
        public bool ConvertValuesToGlobal { get; set; }
        public bool SupportsExecuteMultipleRequest { get; set; }
        public string TempPostfix { get; private set; }
        public IOrganizationService Service { get; private set; }
        public HashSet<int> ValidLanguageCodes { get; private set; }
        private const int CRM2013 = 6;
        private const int CRM2011 = 5;
        private const int Rollup12 = 3218;


        [Flags]
        public enum Steps
        {
            CreateTemp = 1,
            MigrateToTemp = 2,
            RemoveLocal = 4,
            CreateGlobal = 8,
            MigrateToGlobal = 16,
            RemoveTemp = 32
        }

        public Logic(IOrganizationService service, McTools.Xrm.Connection.ConnectionDetail connectionDetail, string tempPostFix, bool migrateData, bool convertValuesToGlobal)
        {
            SupportsExecuteMultipleRequest = connectionDetail.OrganizationMajorVersion >= CRM2013 ||
                                             (connectionDetail.OrganizationMajorVersion >= CRM2011 && int.Parse(connectionDetail.OrganizationVersion.Split(new char[1] { '.' })[3]) >= Rollup12);
            Service = service;
            TempPostfix = tempPostFix;
            MigrateData = migrateData;
            ConvertValuesToGlobal = convertValuesToGlobal;
            ValidLanguageCodes = GetValidLanguageCodes();
        }

        private HashSet<int> GetValidLanguageCodes()
        {
            var resp = (RetrieveAvailableLanguagesResponse)Service.Execute(new RetrieveAvailableLanguagesRequest());
            return new HashSet<int>(resp.LocaleIds);
        }

        public void Run(string entityName, string attributeName, string globalOptionSetName, string globalOptionSetPrefix, Steps stepsToPerform)
        {
            // Create Temp
            AttributeMigrationState state = GetCrmOptionSetLocalToGlobalState(Service, entityName, attributeName, globalOptionSetName);
            var oldAtt = state.Old;
            var tmpAtt = state.Temp;
            var newAtt = state.New;
            AssertValidStepsForState(attributeName, stepsToPerform, state);
            AssertValidGlobalOptionSet(state, stepsToPerform, globalOptionSetName, globalOptionSetPrefix);

            if (stepsToPerform.HasFlag(Steps.CreateTemp))
            {
                tmpAtt = CreateTempAttribute(Service, oldAtt);
            }

            if (oldAtt != null)
            {
                if (stepsToPerform.HasFlag(Steps.MigrateToTemp))
                {
                    // Replace Old Attribute with Tmp Attribute
                    UpdateViews(Service, oldAtt, tmpAtt);
                    UpdateForms(Service, oldAtt, tmpAtt);
                    PublishEntity(Service, oldAtt.EntityLogicalName);
                    AssertCanDelete(Service, oldAtt);
                    if (MigrateData)
                    {
                        CopyData(Service, oldAtt, tmpAtt, globalOptionSetPrefix);
                    }
                }
                if (stepsToPerform.HasFlag(Steps.RemoveLocal))
                {
                    DeleteField(Service, oldAtt);
                }
            }

            // Create new Attribute
            if (stepsToPerform.HasFlag(Steps.CreateGlobal))
            {
                newAtt = newAtt ?? CreateNewAttribute(Service, tmpAtt, state.GlobalOptionSet, globalOptionSetName, globalOptionSetPrefix);
            }

            if (stepsToPerform.HasFlag(Steps.MigrateToGlobal))
            {
                UpdateViews(Service, tmpAtt, newAtt);
                UpdateForms(Service, tmpAtt, newAtt);
                PublishEntity(Service, newAtt.EntityLogicalName);
                AssertCanDelete(Service, tmpAtt);
                if (MigrateData)
                {
                    CopyData(Service, tmpAtt, newAtt, globalOptionSetPrefix);
                }
            }
            if (stepsToPerform.HasFlag(Steps.RemoveTemp))
            {
                DeleteField(Service, tmpAtt);
            }
        }

        private void AssertValidGlobalOptionSet(AttributeMigrationState state, Steps stepsToPerform, string globalOptionSetName, string globalOptionSetPrefix)
        {
            if (MigrateData &&
                (stepsToPerform.HasFlag(Steps.MigrateToGlobal) || stepsToPerform.HasFlag(Steps.MigrateToTemp)))
            {
                var localOptionSet = (state.Old ?? state.Temp);

                if (localOptionSet == null)
                {
                    throw new InvalidOperationException("Unable to Migrate Data!  No local Option Set was found!");
                }

                if (state.GlobalOptionSet != null)
                {
                    if (!state.GlobalOptionSet.IsGlobal.GetValueOrDefault()) // Don't think this is possible
                    {
                        throw new InvalidOperationException("Unable to Migrate Data!  " + globalOptionSetName +
                            " is invalid because it already exists and is not a global option set!");
                    };

                    var globalOptions = GetOptions(state.GlobalOptionSet);

                    foreach (var option in localOptionSet.Options)
                    {
                        var value = option.Value;
                        if (ConvertValuesToGlobal)
                        {
                            if (value > 9999)
                            {
                                throw new InvalidOperationException("Unable to Convert Option Set for Migrate!  " + option.Value +
                                    " is an invalid local set value.  Local Option Set Values can not be greater than 9999");
                            }

                            value = ConvertLocalValueToGlobalValue(globalOptionSetPrefix, value);
                        }

                        if (!globalOptions.Any(osv => osv.Value == option.Value))
                        {
                            var displayText = option.Label == null ? String.Empty : option.Label.GetLocalOrDefaultText("N/A");

                            throw new InvalidOperationException(String.Format("Unable to Migrate Data!  Option Set Value \"{0}\" ({1}) does not have a corresponding value in the global option set {2} ({3})",
                                displayText, option.Value, globalOptionSetName, value));
                        }
                    }
                }
                else if (ConvertValuesToGlobal)
                {
                    foreach (var option in localOptionSet.Options.Where(o => o.Value.GetValueOrDefault() > 9999))
                    {
                        throw new InvalidOperationException("Unable to Convert Option Set for Migrate!  " + option.Value +
                            " is an invalid local set value.  Local Option Set Values can not be greater than 9999");
                    }
                }
            }
        }

        private static List<OptionMetadata> GetOptions(OptionSetMetadataBase optionSet)
        {
            List<OptionMetadata> options;
            var boolOS = optionSet as BooleanOptionSetMetadata;
            if (boolOS == null)
            {
                options = ((OptionSetMetadata)optionSet).Options.ToList();
            }
            else
            {
                options = new List<OptionMetadata>(){
                            boolOS.FalseOption, boolOS.TrueOption
                        };
            }
            return options;
        }

        private AttributeMigrationState GetCrmOptionSetLocalToGlobalState(IOrganizationService service, string entityName, string attributeName, string globalOptionSetName)
        {
            AttributeMigrationState state;
            state = new AttributeMigrationState();
            try
            {
                Trace("Searching for Option Set " + entityName + "." + attributeName);
                
                state.Old = (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName })).AttributeMetadata;
                Trace("Option Set " + entityName + "." + attributeName + " found");
                if (state.Old.OptionSet.IsGlobal.GetValueOrDefault())
                {
                    state.New = state.Old;
                    state.Old = null;
                    state.Temp = GetTempAttribute(service, entityName, attributeName);
                    Trace("Option Set " + entityName + "." + attributeName + " is already a global option set");
                }
                else
                {
                    try
                    {
                        Trace("Searching for Option Set " + entityName + "." + attributeName + TempPostfix);
                        state.Temp = (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName + TempPostfix })).AttributeMetadata;
                        Trace("Option Set " + entityName + "." + attributeName + TempPostfix + " found");
                    }
                    catch { Trace("Option Set " + entityName + "." + attributeName + TempPostfix + " not found");  }
                }
            }
            catch (System.ServiceModel.FaultException<OrganizationServiceFault> ex)
            {
                if (ex.Message.Contains("Could not find attribute"))
                {
                    if (state.New == null)
                    {
                        Trace("Option Set " + entityName + "." + attributeName + " not found.  Seaching for Temp");
                        state.Temp = GetTempAttribute(Service, entityName, attributeName);
                    }
                    else
                    {
                        throw new Exception(state.New.LogicalName + " is already a global option set, and no tmp optionset found to remove");
                    }
                }
                else
                {
                    throw;
                }
            }

            try
            {
                state.GlobalOptionSet = ((RetrieveOptionSetResponse)Service.Execute(new RetrieveOptionSetRequest() { Name = globalOptionSetName })).OptionSetMetadata;
            }
            catch {}
            
            return state;
        }

        private void AssertValidStepsForState(string attributeName, Steps stepsToPerform, AttributeMigrationState state)
        {
            if (stepsToPerform.HasFlag(Steps.CreateTemp) && state.Temp != null)
            {
                throw new InvalidOperationException("Unable to Create Temp!  Temp " + state.Temp.EntityLogicalName + "." + state.Temp.LogicalName + " already exists!");
            }

            if (stepsToPerform.HasFlag(Steps.MigrateToTemp))
            {
                // Can only Migrate if old already exists
                if (state.Old == null)
                {
                    throw new InvalidOperationException("Unable to Migrate!  Local Option Set Value Attribute " + attributeName + " does not exist!");
                }

                // Can only Migrate if Tmp already exists, or temp will be created
                if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
                {
                    throw new InvalidOperationException("Unable to Migrate!  Temporary Attribute " + attributeName + TempPostfix + " does not exist!");
                }
            }


            if (stepsToPerform.HasFlag(Steps.RemoveLocal))
            {
                if (state.Old == null)
                {
                    AssertInvalidState("Unable to Remove Local!  Local Option Set Value Attribute " + attributeName + " does not exist!");
                }

                // Can only Remove local if Tmp already exists, or temp will be created
                if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
                {
                    AssertInvalidState("Unable to Remove Local!  Temporary Attribute " + attributeName + TempPostfix + " does not exist!");
                }

                // Can only Remove local if Tmp will be migrated, or has been migrated
                if (!stepsToPerform.HasFlag(Steps.MigrateToTemp))
                {
                    try
                    {
                        AssertCanDelete(Service, state.Old);
                    }
                    catch
                    {
                        AssertInvalidState("Unable to Remove Local!  Local Option Set Value Attribute " + attributeName + " has not been migrated to Temporary Attribute!");
                    }
                }
            }

            if (stepsToPerform.HasFlag(Steps.CreateGlobal))
            {
                // Can only Create Global if Local does not exist or will be removed
                if (!(state.Old == null || stepsToPerform.HasFlag(Steps.RemoveLocal)))
                {
                    AssertInvalidState("Unable to create Global!  Local Option Set Value Attribute " + attributeName + " still exists!");
                }

                // Can only Create Global if doesn't already exist
                if (!(state.New == null))
                {
                    AssertInvalidState("Unable to create Global!  Global Option Set Value Attribute " + attributeName + " already exists!");
                }
            }

            if (stepsToPerform.HasFlag(Steps.MigrateToGlobal))
            {
                if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
                {
                    AssertInvalidState("Unable to Migrate!  Temp Attribute " + attributeName + TempPostfix + " does not exist!");
                }

                // Can only Migrate if Global Already exists, or Global will be created
                if (!(state.New != null || stepsToPerform.HasFlag(Steps.CreateGlobal)))
                {
                    AssertInvalidState("Unable to Migrate!  Global Option Set Value Attribute " + attributeName + " does not exist!");
                }
            }

            if (stepsToPerform.HasFlag(Steps.RemoveTemp))
            {
                // Can Only remove Temp if it exists, or will exist
                if (!(state.Temp != null || stepsToPerform.HasFlag(Steps.CreateTemp)))
                {
                    AssertInvalidState("Unable to Remove Temp!  Temp Attribute " + attributeName + TempPostfix + " does not exist!");
                }

                // Can Only remove Temp if Global Already exists, or Global will be created
                if (!(state.New != null || stepsToPerform.HasFlag(Steps.CreateGlobal)))
                {
                    AssertInvalidState("Unable to Migrate!  Global Option Set Value Attribute " + attributeName + " does not exist!");
                }

                // Can only Remove tmp if global will be migrated, or has been migrated
                if (!stepsToPerform.HasFlag(Steps.MigrateToGlobal))
                {
                    try
                    {
                        AssertCanDelete(Service, state.Temp);
                    }
                    catch
                    {
                        AssertInvalidState("Unable to Remove Global!  Local Option Set Value Attribute " + attributeName + " has not been migrated to Temporary Attribute!");
                    }
                }
            }
        }

        private void AssertInvalidState(string message)
        {
            throw new InvalidOperationException(message);
        }

        private AttributeMetadata GetTempAttribute(IOrganizationService service, string entityName, string attributeName)
        {
            return (AttributeMetadata)((RetrieveAttributeResponse)service.Execute(new RetrieveAttributeRequest() { EntityLogicalName = entityName, LogicalName = attributeName + TempPostfix })).AttributeMetadata;
        }

        private AttributeMetadata CreateNewAttribute(IOrganizationService service, AttributeMetadata tmpAtt, OptionSetMetadataBase globalOptionSet, string globalOptionSetName, string globalOSPrefix)
        {
            // Create Global OptionSet If needed
            if (globalOptionSet == null)
            {
                globalOptionSet = GetOptionSet(tmpAtt);
                globalOptionSet.IsGlobal = true;
                globalOptionSet.MetadataId = null;
                globalOptionSet.Name = globalOptionSetName;
                if (ConvertValuesToGlobal)
                {
                    foreach (var option in GetOptions(globalOptionSet))
                    {
                            option.Value = ConvertLocalValueToGlobalValue(globalOSPrefix, option.Value);
                    }
                }

                Trace("Creating Global Option Set " + globalOptionSetName);
                try
                {
                    var response = (CreateOptionSetResponse)service.Execute(new CreateOptionSetRequest()
                    {
                        OptionSet = globalOptionSet
                    });
                    globalOptionSet.MetadataId = response.OptionSetId;
                }
                catch
                {
                    Trace("Error Creating Option Set " + globalOptionSet.Name);
                    throw;
                }
            }

            AttributeMetadata att = CloneAttribute(tmpAtt, globalOptionSet, s => s.Replace(TempPostfix, String.Empty));

            ClearOptions(globalOptionSet);
            Trace("Creating Attribute " + att.EntityLogicalName + "." + att.LogicalName);
            try
            {
                service.Execute(new CreateAttributeRequest()
                {
                    Attribute = att,
                    EntityName = att.EntityLogicalName,
                });
            }
            catch
            {

                Trace("Error Creating Attribute " + att.LogicalName);
                throw;
            }

            PublishEntity(service, att.EntityLogicalName);

            return att;
        }

        private void ClearOptions(OptionSetMetadataBase globalOptionSet)
        {
            var options = globalOptionSet as OptionSetMetadata;
            if (options == null)
            {
                var boolOptions = (BooleanOptionSetMetadata)globalOptionSet;
                boolOptions.TrueOption = null;
                boolOptions.FalseOption = null;
            }
            else
            {
                options.Options.Clear();
            }    
        }

        private void DeleteField(IOrganizationService service, AttributeMetadata att)
        {
            Trace("Deleting Field " + att.EntityLogicalName + "." + att.LogicalName);
            service.Execute(new DeleteAttributeRequest()
            {
                EntityLogicalName = att.EntityLogicalName,
                LogicalName = att.LogicalName
            });
        }

        private void AssertCanDelete(IOrganizationService service, AttributeMetadata oldAtt)
        {
            Trace("Checking for Delete Dependencies for " + oldAtt.EntityLogicalName + "." + oldAtt.LogicalName);
            var depends = (RetrieveDependenciesForDeleteResponse)service.Execute(new RetrieveDependenciesForDeleteRequest()
            {
                ComponentType = (int)componenttype.Attribute,
                ObjectId = oldAtt.MetadataId.Value
            });

            var errors = new List<String>();
            foreach (var d in depends.EntityCollection.ToEntityList<Dependency>())
            {
                var type = (componenttype) d.DependentComponentType.GetValueOrDefault();
                errors.Add(type + " " + d.DependentComponentObjectId.Value);
                if (type == componenttype.EntityRelationship)
                {
                    var response =
                        (RetrieveRelationshipResponse) service.Execute(new RetrieveRelationshipRequest {MetadataId = (Guid) d.DependentComponentObjectId});
                    Trace("Entity Relationship {0} must be manually removed/added", response.RelationshipMetadata.SchemaName);
                } 
            }

            if (errors.Count > 0)
            {
                throw new Exception("Dependencies found: " + String.Join(", ", errors));
            }
        }

        private void UpdateForms(IOrganizationService service, AttributeMetadata from, AttributeMetadata to)
        {
            Trace("Retrieving Forms");
            var forms = service.GetEntities<SystemForm>(
                "objecttypecode", from.EntityLogicalName,
                new ConditionExpression("formxml", ConditionOperator.Like, "%<control id=\"" + from.LogicalName + "\"%"));

            foreach (var form in forms)
            {
                Trace("Updating Form " + form.Name);
                form.FormXml = form.FormXml.Replace("<control id=\"" + from.LogicalName + "\"", "<control id=\"" + to.LogicalName + "\"").
                    Replace("datafieldname=\"" + from.LogicalName + "\"", "datafieldname=\"" + to.LogicalName + "\"");
                service.Update(form);
            }
        }

        private void UpdateViews(IOrganizationService service, AttributeMetadata from, AttributeMetadata to)
        {
            Trace("Retrieving Views");
            var queries = service.GetEntities<SavedQuery>(
                new ConditionExpression("fetchxml", ConditionOperator.Like, "%<entity name=\"" + from.EntityLogicalName + "\">%name=\"" + from.LogicalName + "\"%"));

            foreach (var query in queries)
            {
                Trace("Updating View " + query.Name);
                query.FetchXml = query.FetchXml.Replace("name=\"" + from.LogicalName + "\"", "name=\"" + to.LogicalName + "\"");
                query.FetchXml = query.FetchXml.Replace("attribute=\"" + from.LogicalName + "\"", "attribute=\"" + to.LogicalName + "\"");
                if (query.LayoutXml != null)
                {
                    query.LayoutXml = query.LayoutXml.Replace("name=\"" + from.LogicalName + "\"", "name=\"" + to.LogicalName + "\"");
                }
                service.Update(query);
            }
        }

        private void CopyData(IOrganizationService service, OptionSetAttributeContainer from, OptionSetAttributeContainer to, string globalOSPrefix)
        {
            if (!MigrateData) { return; }

            var total = GetRecordCount(service, from);
            var count = 0;

            Trace("Copying data from {0} to {1}", from.LogicalName, to.LogicalName);
            var requests = new OrganizationRequestCollection();
            // Grab from and to, and only update if not equal.  This is to speed things up if it has failed part way through
            foreach (var entity in service.RetrieveAllList<Entity>(new QueryExpression(from.EntityLogicalName) { ColumnSet = new ColumnSet(from.LogicalName, to.LogicalName)}))
            {
                if (count++%100 == 0 || count == total)
                {
                    if (requests.Any())
                    {
                        PerformUpdates(service, requests);
                    }

                    Trace("Copying {0} / {1}", count, total);
                    requests.Clear();
                }

                var value = entity.GetAttributeValue<OptionSetValue>(from.LogicalName);
                var toValue = entity.GetAttributeValue<OptionSetValue>(to.LogicalName);
                if (value != null)
                {
                    if (ConvertValuesToGlobal && to.OptionSet.IsGlobal.GetValueOrDefault() && !from.OptionSet.IsGlobal.GetValueOrDefault())
                    {
                        value = new OptionSetValue(ConvertLocalValueToGlobalValue(globalOSPrefix, value.Value).GetValueOrDefault());
                    }

                    if (!value.Equals(toValue))
                    {
                        entity.Attributes[to.LogicalName] = value;
                        requests.Add(new UpdateRequest() {Target = entity});
                    }
                }
                else if (toValue != null)
                {
                    entity.Attributes[to.LogicalName] = null;
                    requests.Add(new UpdateRequest() { Target = entity });
                }
            }

            if (requests.Any())
            {
                PerformUpdates(service, requests);
            }

            Trace("Data Migration Complete", count, total);
        }

        private void PerformUpdates(IOrganizationService service, OrganizationRequestCollection requests)
        {
            if (SupportsExecuteMultipleRequest)
            {
                var response = (ExecuteMultipleResponse) service.Execute(
                    new ExecuteMultipleRequest()
                    {
                        Settings = new ExecuteMultipleSettings()
                        {
                            ContinueOnError = false,
                            ReturnResponses = false
                        },
                        Requests = requests,
                    });

                if (response.IsFaulted)
                {
                    var fault = response.Responses.First().Fault;
                    while (fault.InnerFault != null)
                    {
                        fault = fault.InnerFault;
                    }

                    var errorDetails = String.Empty;
                    if (fault.ErrorDetails.ContainsKey("CallStack"))
                    {
                        errorDetails = Environment.NewLine + fault.ErrorDetails["CallStack"];
                    }
                    throw new Exception(fault.Message + errorDetails);
                }
            }
            else
            {
                foreach (var request in requests)
                {
                    service.Save(((UpdateRequest) request).Target);
                }
            }
        }

        private static int? ConvertLocalValueToGlobalValue(string globalOSPrefix, int? value)
        {
            if (value.HasValue)
            {
                return int.Parse(globalOSPrefix + value.ToString().PadLeft(4, '0'));
            }
            else
            {
                return null;
            }
        }

        private int GetRecordCount(IOrganizationService service, AttributeMetadata from)
        {
            Trace("Retrieving {0} id attribute name", from.EntityLogicalName);
            var response = (RetrieveEntityResponse)service.Execute(new RetrieveEntityRequest() { LogicalName = from.EntityLogicalName, EntityFilters = EntityFilters.Entity });

            Trace("Determining record count (accurate only up to 50000)");
            var xml = String.Format(@"
            <fetch distinct='false' mapping='logical' aggregate='true'> 
                <entity name='{0}'> 
                   <attribute name='{1}' alias='{1}_count' aggregate='count'/> 
                </entity> 
            </fetch>", from.EntityLogicalName, response.EntityMetadata.PrimaryIdAttribute);

            int total;
            try
            {
                var resultEntity = service.RetrieveMultiple(new FetchExpression(xml)).Entities.First();
                total = resultEntity.GetAliasedValue<int>(response.EntityMetadata.PrimaryIdAttribute + "_count");
            }
            catch (Exception ex)
            {
                if (ex.Message.Contains("AggregateQueryRecordLimit exceeded"))
                {
                    total = 50000;
                }
                else
                {
                    throw;
                }
            }
            
            return total;
        }

        private OptionSetAttributeContainer CreateTempAttribute(IOrganizationService service, AttributeMetadata oldAtt)
        {
            var optionSet = GetOptionSet(oldAtt);
            optionSet.MetadataId = null;
            optionSet.Name = optionSet.Name + TempPostfix;
            AttributeMetadata att = CloneAttribute(oldAtt, optionSet, n => n += TempPostfix);

            var createAttReq = new CreateAttributeRequest()
            {
                EntityName = att.EntityLogicalName,
                Attribute = att
            };

            try
            {
                var response = (CreateAttributeResponse)service.Execute(createAttReq);
                att.MetadataId = response.AttributeId;
            }
            catch
            {
                Trace("Error Creating Attribute " + att.LogicalName);
                throw;
            }

            // http://msdn.microsoft.com/en-us/library/microsoft.crm.sdk.messages.publishxmlrequest.parameterxml.aspx

            PublishEntity(service, oldAtt.EntityLogicalName);
            return att;
        }

        private AttributeMetadata CloneAttribute(AttributeMetadata att, OptionSetMetadataBase optionSet, Func<string, string> convertName)
        {
            AttributeMetadata clone = CopyAttribute(att, optionSet);
            clone.CanModifyAdditionalSettings = att.CanModifyAdditionalSettings;
            clone.Description = att.Description;
            clone.DisplayName = att.DisplayName;
            clone.ExtensionData = att.ExtensionData;
            clone.IsAuditEnabled = att.IsAuditEnabled;
            clone.IsCustomizable = att.IsCustomizable;
            clone.IsRenameable = att.IsRenameable;
            clone.IsSecured = att.IsSecured;
            clone.IsValidForAdvancedFind = att.IsValidForAdvancedFind;
            clone.LinkedAttributeId = att.LinkedAttributeId;
            clone.RequiredLevel = att.RequiredLevel;

            // Fix for issue 1468 Inactive Language Causing Error
            RemoveInvalidLanguageLocalizedLabels(att.Description);
            RemoveInvalidLanguageLocalizedLabels(att.DisplayName);

            clone.LogicalName = convertName(att.LogicalName);
            clone.SchemaName = convertName(att.SchemaName);

            // Update EntityLogicalName for other methods to use
            SetEntityLogicalName(clone, att.EntityLogicalName);
            return clone;
        }

        private AttributeMetadata CopyAttribute(AttributeMetadata existingAtt, OptionSetMetadataBase optionSet)
        {
            AttributeMetadata att;
            switch (existingAtt.AttributeType.GetValueOrDefault())
            {
                case AttributeTypeCode.Boolean:
                    var boolAtt = (BooleanAttributeMetadata)existingAtt;
                    var tmpBool = new BooleanAttributeMetadata()
                    {
                        DefaultValue = boolAtt.DefaultValue,
                        OptionSet = (BooleanOptionSetMetadata)optionSet,
                    };

                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.Description);
                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.DisplayName);
                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.TrueOption.Description);
                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.TrueOption.Label);
                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.FalseOption.Description);
                    RemoveInvalidLanguageLocalizedLabels(tmpBool.OptionSet.FalseOption.Label);
                    att = tmpBool;
                    break;

                case AttributeTypeCode.Picklist:
                    att = new PicklistAttributeMetadata();
                    break;

                case AttributeTypeCode.State:
                    att = new StateAttributeMetadata();
                    break;

                case AttributeTypeCode.Status:
                    att = new StatusAttributeMetadata();
                    break;

                default:
                    throw new EnumCaseUndefinedException<AttributeTypeCode>(existingAtt.AttributeType.GetValueOrDefault());
            }

            EnumAttributeMetadata enumAtt = existingAtt as EnumAttributeMetadata;
            if (enumAtt != null)
            {
                var tmp = att as EnumAttributeMetadata;
                tmp.DefaultFormValue = enumAtt.DefaultFormValue;
                tmp.OptionSet = (OptionSetMetadata)optionSet;

                // Fix for issue 1468 Inactive Language Causing Error
                RemoveInvalidLanguageLocalizedLabels(tmp.OptionSet.Description);
                RemoveInvalidLanguageLocalizedLabels(tmp.OptionSet.DisplayName);
                foreach (var label in tmp.OptionSet.Options.Select(o => o.Label))
                {
                    RemoveInvalidLanguageLocalizedLabels(label);
                }
                foreach (var description in tmp.OptionSet.Options.Select(o => o.Description))
                {
                    RemoveInvalidLanguageLocalizedLabels(description);
                }
            }

            return att;
        }

        private void RemoveInvalidLanguageLocalizedLabels(Label label)
        {
            if (label == null)
            {
                return;
            }

            var labelsToRemove = new List<LocalizedLabel>();
            foreach (var local in label.LocalizedLabels)
            {
                if (!ValidLanguageCodes.Contains(local.LanguageCode))
                {
                    labelsToRemove.Add(local);
                }
            }

            if (label.UserLocalizedLabel != null && !ValidLanguageCodes.Contains(label.UserLocalizedLabel.LanguageCode))
            {
                Trace("UserLocalizedLabel was invalid.  Removing Localization Label '{0}' for language code '{1}'", label.UserLocalizedLabel.Label, label.UserLocalizedLabel.LanguageCode);
                label.UserLocalizedLabel = null;
            }

            foreach (var local in labelsToRemove)
            {
                Trace("Removing Localization Label '{0}' for language code '{1}'", local.Label, local.LanguageCode);
                label.LocalizedLabels.Remove(local);
            }

            labelsToRemove.Clear();
        }

        private OptionSetMetadataBase GetOptionSet(AttributeMetadata att)
        {
            OptionSetMetadataBase optionSet = null;
            switch (att.AttributeType.GetValueOrDefault())
            {
                case AttributeTypeCode.Boolean:
                    optionSet = ((BooleanAttributeMetadata)att).OptionSet;
                    break;

                case AttributeTypeCode.Picklist:
                case AttributeTypeCode.EntityName:
                case AttributeTypeCode.State:
                case AttributeTypeCode.Status:
                    optionSet = ((EnumAttributeMetadata)att).OptionSet;
                    break;
            }
            return optionSet;
        }

        private void SetEntityLogicalName(AttributeMetadata att, string entityLogicalName)
        {
            var prop = att.GetType().GetProperty("EntityLogicalName");
            prop.SetValue(att, entityLogicalName);
        }

        private void PublishEntity(IOrganizationService service, string logicalName)
        {
            Trace("Publishing Entity " + logicalName);
            service.Execute(new PublishXmlRequest()
            {
                ParameterXml = "<importexportxml>"
              + "    <entities>"
              + "        <entity>" + logicalName + "</entity>"
              + "    </entities>"
              + "</importexportxml>"
            });
        }

        private void Trace(string message)
        {
            OnLog(message);
        }

        private void Trace(string messageFormat, params object[] args)
        {
            OnLog(String.Format(messageFormat, args));
        }

        private class OptionSetAttributeContainer
        {
            public AttributeMetadata Attribute { get; set; }
            public List<OptionMetadata> Options { get; set; }
            public OptionSetMetadataBase OptionSet { get; private set; }

            public string LogicalName { get { return Attribute.LogicalName; } }
            public string EntityLogicalName { get { return Attribute.EntityLogicalName; } }

            private OptionSetMetadataBase GetOptionSet(AttributeMetadata att)
            {
                OptionSetMetadataBase value = null;
                if (att != null)
                {
                    switch (att.AttributeType.GetValueOrDefault())
                    {
                        case AttributeTypeCode.Boolean:
                            var boolOptionSet = ((BooleanAttributeMetadata)att).OptionSet;
                            Options = new List<OptionMetadata>(){
                                boolOptionSet.FalseOption, boolOptionSet.TrueOption
                            };
                            value = boolOptionSet;
                            break;
                        case AttributeTypeCode.State:
                        //case AttributeTypeCode.EntityName:
                        case AttributeTypeCode.Picklist:
                        case AttributeTypeCode.Status:
                            var enumOptionSet = ((EnumAttributeMetadata)att).OptionSet;
                            Options = enumOptionSet.Options.ToList();
                            value = enumOptionSet;
                            break;
                        default:
                            throw new EnumCaseUndefinedException<AttributeTypeCode>(att.AttributeType.GetValueOrDefault());
                    }
                }
                return value;
            }

            public static implicit operator OptionSetAttributeContainer(AttributeMetadata att)
            {
                var container = new OptionSetAttributeContainer() { Attribute = att };
                container.OptionSet = container.GetOptionSet(att);
                return container;
            }

            public static implicit operator AttributeMetadata(OptionSetAttributeContainer container)
            {
                return container.Attribute;
            }
        }

        private class AttributeMigrationState
        {
            public OptionSetAttributeContainer Old { get; set; }
            public OptionSetAttributeContainer Temp { get; set; }
            public OptionSetAttributeContainer New { get; set; }
            public OptionSetMetadataBase GlobalOptionSet { get; set; }
        }
    }
}
